﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace DigitalGenetics
{
    public partial class SumMatrix
    {
        const int SumMatrixSize = 256;
        string transTable = "tcag";

        private int[,] _CurrentSumMatrix;
        public int[,] CurrentSumMatrix
        {
            get { return _CurrentSumMatrix; }
            internal set { _CurrentSumMatrix = value; }
        }

        private int[] _SerializedSumMatrix;
        public int[] SerializedSumMatrix
        {
            get
            {
                if (_SerializedSumMatrix == null)
                {
                    _SerializedSumMatrix = new int[SumMatrixSize * SumMatrixSize];
                    Buffer.BlockCopy(CurrentSumMatrix, 0, _SerializedSumMatrix, 0, CurrentSumMatrix.Length * sizeof(int));
                }
                return _SerializedSumMatrix;
            }
        }

        int[] _sortedSumMatrix;
        public int[] SortedSumMatrix
        {
            get
            {
                if (_sortedSumMatrix == null)
                {
                    _sortedSumMatrix = SerializedSumMatrix.OrderBy(v => v).ToArray();
                }
                return _sortedSumMatrix;
            }

        }

        int _smLevel = 8; // 1 - 8
        public int SmLevel
        {
            get
            {
                return _smLevel;
            }
            set
            {
                _smLevel = value;
                if (_smLevel < 1) _smLevel = 1;
                if (_smLevel > 8) _smLevel = 8;
            }
        }

        int _greys = 0;
        public int greys
        {
            get
            {
                if (_greys == 0)
                {
                    _greys = (int)Math.Pow(2, SmLevel);
                }
                return _greys;
            }
        }

        int[] _medianValues = null;
        public int[] MedianValues
        {
            get
            {
                if (_medianValues == null)
                {
                    _medianValues = new int[greys];
                    int step = SortedSumMatrix.Length / greys;
                    for (int i = 1; i < greys; i++)
                    {
                        _medianValues[i - 1] = SortedSumMatrix[i * step];
                    }
                    _medianValues[greys - 1] = SortedSumMatrix[SortedSumMatrix.Length - 1];
                }
                return _medianValues;
            }
        }

        int[,] _normalizedSumMatrix = null;
        public int[,] NormalizedSumMatrix
        {
            get
            {
                if (_normalizedSumMatrix == null)
                {
                    _normalizedSumMatrix = new int[256, 256];
                    for (int x = 0; x < 256; x++)
                    {
                        for (int y = 0; y < 256; y++)
                        {
                            int aVal = (from m in MedianValues
                                        where m >= CurrentSumMatrix[x, y]
                                        select m
                                            ).FirstOrDefault();

                            int a = Array.IndexOf(MedianValues, aVal);
                            if (a >= greys) a = greys - 1;
                            //a = 255 - a;
                            _normalizedSumMatrix[x, y] = a;
                        }
                    }

                }
                return _normalizedSumMatrix;
            }
        }

        public String SourceSmFileName { get; set; }

        public int FastaLinesRead { get; set; } // 

        private long progressLines { get; set; }
        public event ReadLinesHandler ReadLines;
        public delegate void ReadLinesHandler(SumMatrix sm, int readLines);
        protected virtual void OnReadLines(int readLines)
        {
            if (ReadLines != null)
            {
                ReadLines(this, readLines);
            }
        }

        private void Init(string smFileName = null)
        {
            _CurrentSumMatrix = new int[SumMatrixSize, SumMatrixSize];
            SumMatrixClear();
            SourceSmFileName = smFileName;
            FastaLinesRead = 0;
            progressLines = 1;

        }

        public SumMatrix(int level = 8)
        {
            SmLevel = level;
            Init();
        }

        public SumMatrix(String fastaFile, String hexFile, String smFile = null, int level = 8)
        {
            SmLevel = level;
            Open(fastaFile, hexFile, smFile);
        }

        /// <summary>
        /// Delayed object creation
        /// </summary>
        /// <param name="fastaFile"></param>
        /// <param name="hexFile"></param>
        /// <param name="smFile"></param>
        public void Open(String fastaFile, String hexFile, String smFile = null)
        {
            Init(smFile);
            Fasta2Hex(fastaFile, hexFile, smFile);
        }

        private SumMatrix(FileStream fastaFile, FileStream hexFile, FileStream smFile = null)
        {
            Open(fastaFile, hexFile, smFile);
        }

        private void Open(FileStream fastaFile, FileStream hexFile, FileStream smFile)
        {
            Init();
            Fasta2Hex(fastaFile, hexFile, smFile);
        }

        public SumMatrix(String smFile, int level = 8)
        {
            SmLevel = level;
            Init(smFile);
            // C:\Temp\Genes\Human\chrY.fa.tcag.n-t.hex.sm
            // parse comma-separated file
            using (FileStream fs = new FileStream(smFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (TextReader smReader = new StreamReader(fs))
            {
                string line = smReader.ReadLine();
                char[] separator = new char[] { ',', '\t' };
                int y = 0;
                while (!string.IsNullOrEmpty(line))
                {
                    string[] xLine = line.Split(separator);
                    if (xLine.Length != _CurrentSumMatrix.GetLength(0))
                    {
                        throw new InvalidDataException("This is not a SumMatrix file");
                    }

                    for (int x = 0; x < xLine.Length; x++)
                    {
                        _CurrentSumMatrix[x, y] = int.Parse(xLine[x]);
                    }
                    line = smReader.ReadLine();
                    y++;
                }
            }

        }

        static public void HexTo4_3(String hexFile, String fourThirdFile)
        {
            using (FileStream fs = new FileStream(hexFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                using (FileStream write = new FileStream(fourThirdFile, FileMode.Create))
                {
                    HexTo4_3(fs, write);
                }
            }
        }

        static private void HexTo4_3(FileStream hexStream, FileStream fourThirdStream)
        {
            //const int hexBufSize = 1024 * 16;
            //byte[] hexBuf = new byte[hexBufSize];
            long ind = 0;
            using (BinaryReader hexReader = new BinaryReader(hexStream))
            {
                using (BinaryWriter fourThirdWriter = new BinaryWriter(fourThirdStream))
                {
                    long length = hexReader.BaseStream.Length;
                    // transform every 3 input bytes into 4 output bytes
                    byte[] threeBytes=null; // = new byte[3];
                    byte[] fourBytes = new byte[4];

                    // for now: skip head 0s
                    while (hexReader.ReadByte() == 0) { };
                    hexReader.BaseStream.Position -= 1; // first non-0
                    ind = hexReader.BaseStream.Position;
                    while (ind < length)
                    {
                        try
                        {
                            threeBytes = hexReader.ReadBytes(3);

                        }
                        catch (Exception ex)
                        {
                            string msg = ex.Message;
                            break;
                        }
                        if (threeBytes.Length < 3)
                        {
                            // for now do not process the tail (last 0-3 bytes) of the input file
                            break;
                        }
                        // skip middle 0s
                        if (threeBytes[0] == 0 && threeBytes[1] == 0 && threeBytes[2] == 0)
                        {
                            try
                            {
                                while (hexReader.ReadByte() == 0) { };

                            }
                            catch (Exception ex)
                            {
                                string msg = ex.Message;
                            }
                            ind = hexReader.BaseStream.Position;
                            continue;
                        }
                        ind += 3;
                        fourBytes[0] = (byte)(threeBytes[0] >> 2);
                        fourBytes[1] = (byte)((threeBytes[0] & 0x03) << 4 | (threeBytes[1] & 0xF0) >> 4);
                        fourBytes[2] = (byte)((threeBytes[1] & 0x0F) << 2 | (threeBytes[2] & 0xC0) >> 6);
                        fourBytes[3] = (byte)(threeBytes[2] & 0x3F);

                        fourThirdWriter.Write(fourBytes);
                    }


                    fourThirdWriter.Close();
                }
            }
        }

        public void Fasta2Hex(String fastaFile, String hexFile, String smFile = null)
        {
            using (FileStream fs = new FileStream(fastaFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                using (FileStream write = new FileStream(hexFile, FileMode.Create))
                {
                    if (string.IsNullOrEmpty(smFile))
                    {
                        Fasta2Hex(fs, write);
                    }
                    else
                    {
                        using (FileStream writeSM = new FileStream(smFile, FileMode.Create))
                        {
                            Fasta2Hex(fs, write, writeSM);
                        }
                    }
                }
            }
        }

        private void Fasta2Hex(FileStream fastaFile, FileStream hexFile, FileStream smFile = null)
        {
            using (TextReader fastaReader = new StreamReader(fastaFile))
            {

                string line = NextLine(fastaReader);
                string tail = "";

                bool firstLine = true;
                long nLines = 1;

                SumMatrixClear();

                while (!string.IsNullOrEmpty(line))
                {
                    if (line.StartsWith(">"))
                    {
                        line = NextLine(fastaReader);
                        continue;
                    }

                    if (firstLine)
                    {
                        firstLine = false;

                        // estimate total number of lines
                        nLines = fastaFile.Length / line.Length;
                        progressLines = nLines / 100;
                        if (progressLines < 1) progressLines = 1;
                    }

                    line = line.Replace("N", "t"); // N=aNy, ->0
                    string fourPack;
                    char[] fourChar;
                    indByte = 0; // for AddToSumMatrix
                    for (int i = 0; i < (line.Length / 4 + 1); i++)
                    {
                        tail = line.Substring(i * 4);
                        if (tail.Length >= 4)
                        {
                            fourPack = tail.Substring(0, 4).ToLower();
                            fourChar = fourPack.ToCharArray();
                            int iByte =
                                transTable.IndexOf(fourChar[3]) +
                                (transTable.IndexOf(fourChar[2]) << 2) +
                                (transTable.IndexOf(fourChar[1]) << 4) +
                                (transTable.IndexOf(fourChar[0]) << 6);
                            if (iByte >= 0 && iByte < 256)
                            {
                                byte bByte = (byte)iByte;
                                char oneByte = (char)bByte;
                                WriteNext(hexFile, oneByte);

                                // create SumMatrix on a fly
                                AddToSumMatrix(bByte);
                            }
                        }
                    }

                    string nextStr = NextLine(fastaReader);
                    if (string.IsNullOrEmpty(nextStr))
                    { // end of file
                        foreach (char ch in tail.ToCharArray())
                        {
                            WriteNext(hexFile, ch);

                            // create SumMatrix on a fly
                            AddToSumMatrix((byte)ch);
                        }
                        line = null;
                    }
                    else
                    {
                        line = tail + nextStr;
                    }
                }

                WriteNext(hexFile, '.', true); // force to write the tail
                hexFile.Close();
                fastaReader.Close();

                if (smFile != null)
                {
                    // write SumMatrix comma-separated file
                    WriteSumMatrix(smFile);
                }
            }
        }

        /// <summary>
        /// write SumMatrix tab-separated file
        /// </summary>
        /// <param name="smFile">Output sm file name</param>
        private void WriteSumMatrix(FileStream smFile)
        {
            using (TextWriter textSM = new StreamWriter(smFile))
            {
                StringBuilder sb = new StringBuilder();
                for (int y = 0; y < SumMatrixSize; y++)
                {
                    for (int x = 0; x < SumMatrixSize; x++)
                    {
                        sb.AppendFormat("{0}\t", _CurrentSumMatrix[x, y]);
                    }
                    string aLine = sb.ToString();
                    aLine = aLine.TrimEnd('\t');
                    textSM.WriteLine(aLine);
                    sb.Clear();
                }
                textSM.Close();
            }
        }

        public void Hex2Sm(String hexFile, String smFile = null)
        {
            using (FileStream fsHexFile = new FileStream(hexFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                if (string.IsNullOrEmpty(smFile))
                {
                    Hex2Sm(fsHexFile);
                }
                else
                {
                    using (FileStream writeSM = new FileStream(smFile, FileMode.Create))
                    {
                        Hex2Sm(fsHexFile, writeSM);
                    }
                }
            }
        }

        private void Hex2Sm(FileStream fsHexFile, FileStream writeSM = null)
        {
            int ch = fsHexFile.ReadByte();
            SumMatrixClear();
            indByte = 0; // for AddToSumMatrix
            while (ch >= 0)
            {
                AddToSumMatrix((byte)ch);
                ch = fsHexFile.ReadByte();
            }

            if (writeSM != null)
            {
                WriteSumMatrix(writeSM);
            }
        }

        public bool IsS16(int x, int y)
        {
            return (0x70 <= x && x < 0x80) || (0x70 <= y && y < 0x80);
        }

        public bool IsS16Cross(int x, int y)
        {
            return (0x70 <= x && x < 0x80) && (0x70 <= y && y < 0x80);
        }

        public bool IsS4(int x, int y)
        {
            int offset = 32;
            int width = 4;
            int num = 4;
            return InPattern(x, y, offset, width, num);
        }

        public bool IsS4Cross(int x, int y)
        {
            int offset = 32;
            int width = 4;
            int num = 4;
            return InPatternCross(x, y, offset, width, num);
        }

        public bool IsS1(int x, int y)
        {
            int offset = 8;
            int width = 1;
            int num = 16;
            return InPattern(x, y, offset, width, num);
        }

        public bool IsS1Cross(int x, int y)
        {
            int offset = 8;
            int width = 1;
            int num = 16;
            return InPatternCross(x, y, offset, width, num);
        }

        private static bool IsF1(int x, int y)
        {
            int offset = 2;
            int width = 1;
            int num = 64;
            int gap = SumMatrixSize / num;
            for (int i = 0; i < num; i++)
            {
                if (((offset + i * gap - width <= x) && (x < offset + i * gap)) && y >= 0xC0)
                {
                    return true;
                }
            }
            return false;
        }

        private static bool InPattern(int x, int y, int offset, int width, int num)
        {
            int gap = SumMatrixSize / num;
            for (int i = 0; i < num; i++)
            {
                if (((offset + i * gap - width <= x) && (x < offset + i * gap)) ||
                    ((offset + i * gap - width <= y) && (y < offset + i * gap))
                    )
                {
                    return true;
                }
            }
            return false;
        }

        private static bool InPatternCross(int x, int y, int offset, int width, int num)
        {
            int gap = SumMatrixSize / num;
            for (int i = 0; i < num; i++)
            {
                if (((offset + i * gap - width <= x) && (x < offset + i * gap)) &&
                    ((offset + i * gap - width <= y) && (y < offset + i * gap))
                    )
                {
                    return true;
                }
            }
            return false;
        }

        public void GetMetrics(Metrics metrics)
        {
            metrics.Name = DirFileCaption(SourceSmFileName) + ",";
            for (int x = 0; x < SumMatrixSize; x++)
            {
                for (int y = 0; y < SumMatrixSize; y++)
                {
                    AddMetric(metrics.Total, x, y, CheckZero: false);

                    // S16
                    if (IsS16(x, y))
                    {
                        AddMetric(metrics.S16metric, x, y);
                        //continue;
                    }
                    if (IsS16Cross(x, y))
                    {
                        AddMetric(metrics.S16metricCross, x, y);
                    }

                    // S4
                    if (IsS4(x, y))
                    {
                        AddMetric(metrics.S4metric, x, y);
                        //continue;
                    }
                    if (IsS4Cross(x, y))
                    {
                        AddMetric(metrics.S4metricCross, x, y);
                    }

                    // S1
                    if (IsS1(x, y))
                    {
                        AddMetric(metrics.S1metric, x, y);
                        //continue;
                    }
                    if (IsS1Cross(x, y))
                    {
                        AddMetric(metrics.S1metricCross, x, y);
                    }

                    // F1
                    if (IsF1(x, y))
                    {
                        AddMetric(metrics.F1metric, x, y);
                    }

                }
            }

            metrics.MakePercent();

        }

        private void AddMetric(Metric metric, int x, int y, bool CheckZero = false)
        {
            if (_CurrentSumMatrix[x, y] > 0 || !CheckZero)
            {
                metric.Count++;
            }
            metric.Value += _CurrentSumMatrix[x, y];
        }

        byte[] twoBytes = new byte[2]; int indByte = 0;
        private int AddToSumMatrix(byte bByte)
        {
            if (indByte == 0)
            {
                twoBytes[0] = bByte;
                indByte++;
            }
            else
            {
                twoBytes[1] = bByte;
                indByte = 0;
                _CurrentSumMatrix[twoBytes[0], twoBytes[1]]++;
            }
            return indByte;
        }

        private string NextLine(TextReader read)
        {
            string line = read.ReadLine();
            FastaLinesRead++;
            if (FastaLinesRead % progressLines == 0 || string.IsNullOrEmpty(line))
            {
                OnReadLines(FastaLinesRead);
            }
            return line;
        }

        private void SumMatrixClear()
        {
            for (int x = 0; x < SumMatrixSize; x++)
            {
                for (int y = 0; y < SumMatrixSize; y++)
                {
                    _CurrentSumMatrix[x, y] = 0;
                }
            }
        }

        List<byte> outArr = new List<byte>();
        private void WriteNext(FileStream write, char oneByte, bool final = false)
        {
            if (!final) { outArr.Add((byte)oneByte); }
            if (outArr.Count >= 8 || final)
            {
                write.Write(outArr.ToArray(), 0, outArr.Count);
                outArr.Clear();
            }
        }

        private string DirFileCaption(string fullFileName)
        {
            string fName = Path.GetFileName(fullFileName);
            string dir = Path.GetDirectoryName(fullFileName);
            string[] aDir = dir.Split('\\');
            string caption = string.Format("{0} \\ {1}", aDir[aDir.Length - 1], fName);
            return caption;
        }

    }
}
